package bancosys.tec.rtm.monitor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Implementacao de monitor baseado em checkers (verificadores de estado simples, Ok/NaoOk).
 * 
 * @author Marcio Ribeiro (mmr)
 */
public class CheckerMonitorImpl extends AbstractMonitor implements CheckerMonitor {
    // Map<String, Checker>
    private final Map<String, Checker> checkers = new HashMap<String, Checker>();

    // Map<String, Action>
    private final Map<String, Action> actions = new LinkedHashMap<String, Action>();

    // List<RulesCase>
    private final List<RulesCase> rulesCases = new ArrayList<RulesCase>();

    private long refreshInterval = 0;

    /**
     * Construtor. Se construido dessa forma o monitor nao tera seu estado atualizado automaticamente (refreshInterval = 0).
     */
    public CheckerMonitorImpl() {
    }

    /**
     * Construtor.
     * 
     * @param refreshInterval tempo em segundos para intervalo de chamada do metodo refresh.
     */
    public CheckerMonitorImpl(long refreshInterval) {
        this.refreshInterval = refreshInterval;
    }

    /**
     * @return mapa de checkers.
     */
    public Map<String, Checker> getCheckers() {
        return this.checkers;
    }

    /**
     * @return mapa de actions.
     */
    public Map<String, Action> getActions() {
        return this.actions;
    }

    /**
     * @return lista de casos de regras.
     */
    public List<RulesCase> getRulesCases() {
        return this.rulesCases;
    }

    /**
     * Adiciona um checker ao mapa de checkers.
     * 
     * @param checkerKey chave que sera usada para recuperar esse checker.
     * @param checker instancia do checker.
     */
    public void addChecker(String checkerKey, Checker checker) {
        if (this.checkers.get(checkerKey) != null) {
            throw new CheckerAlreadyAddedException(checkerKey);
        }
        this.checkers.put(checkerKey, checker);
    }

    /**
     * Adiciona uma action ao mapa de actions.
     * 
     * @param actionKey chave que sera usada para recuperar essa action.
     * @param action instancia da action.
     */
    public void addAction(String actionKey, Action action) {
        if (this.actions.get(actionKey) != null) {
            throw new ActionAlreadyAddedException(actionKey);
        }
        this.actions.put(actionKey, action);
    }

    /**
     * Adiciona um caso de regras.
     * 
     * @param rulesCase um caso de regras.
     */
    public void addRulesCase(RulesCase rulesCase) {
        this.rulesCases.add(rulesCase);
    }

    /**
     * @return tempo em milisegundos que o monitor deve ter seu estado atualizado.
     * @see Monitor#getRefreshInterval()
     */
    public long getRefreshInterval() {
        return this.refreshInterval;
    }

    /**
     * Itera por todos checkers verificando estado (Ok/NaoOk), acionando uma acao, se satisfizer regras.
     */
    public synchronized void refresh() {
        // Map<String, Boolean>
        final Map<String, Boolean> currentValues = this.getCurrentValues();

        // Itera por casos de regras, executando actions que tiverem as regras satisfeitas
        for (Iterator<RulesCase> it = this.rulesCases.iterator(); it.hasNext();) {
            RulesCase rulesCase = it.next();
            if (this.shouldRunActions(currentValues, rulesCase)) {
                this.runActions(rulesCase);
            }
        }
    }

    /**
     * Executa actions registradas para o caso de regras passado.
     * 
     * @param rulesCase caso de regras.
     */
    private void runActions(RulesCase rulesCase) {
        for (Iterator<String> itActionKeys = rulesCase.getActions().iterator(); itActionKeys.hasNext();) {
            String actionKey = itActionKeys.next();
            Action action = this.actions.get(actionKey);
            if (action == null) {
                throw new ActionNotFoundException(actionKey);
            }
            // XXX (mmr) iniciar cada action em thread separada?
            action.runAction();
        }
    }

    /**
     * Verifica se deve rodar action de acordo com regras configuradas.
     * 
     * @param currentValues mapa com valores correntes dos checkers.
     * @param rulesCase caso de regras.
     * @return <code>true</code> se deve executar action, <code>false</code> se nao.
     */
    private boolean shouldRunActions(final Map<String, Boolean> currentValues, RulesCase rulesCase) {
        boolean shouldRunAction = false;
        for (Iterator<Rule> itRules = rulesCase.getRules().iterator(); itRules.hasNext();) {
            Rule rule = itRules.next();
            Boolean checkerIsOk = currentValues.get(rule.getCheckerKey());
            if (checkerIsOk == null) {
                throw new CheckerNotFoundException(rule.getCheckerKey());
            }

            // Verificando se estado do checker satisfaz estado da regra
            if (checkerIsOk.booleanValue() == rule.isOk()) {
                shouldRunAction = true;

                // Se o operador logico entre regras NAO for AND (ie. OR), nao precisa testar outras regras
                if (!rulesCase.isLogicalOperatorAnd()) {
                    break;
                }
            } else if (rulesCase.isLogicalOperatorAnd()) {
                // Operador logico eh AND e regra falhou, nao precisa testar outras regras
                // A action nao sera executada
                shouldRunAction = false;
                break;
            }
        }
        return shouldRunAction;
    }

    /**
     * Devolve mapa com valores correntes dos checkers configurados.
     * 
     * @return mapa com valores correntes dos checkers configurados.
     */
    private Map<String, Boolean> getCurrentValues() {
        final Map<String, Boolean> currentValues = new HashMap<String, Boolean>();

        // Itera por todos checkers, guarda valor corrente do estado de cada checker em mapa currentValues
        for (Iterator<Entry<String, Checker>> it = this.checkers.entrySet().iterator(); it.hasNext();) {
            Entry<String, Checker> mapEntry = it.next();
            String checkerKey = mapEntry.getKey();
            Checker checker = mapEntry.getValue();
            currentValues.put(checkerKey, new Boolean(checker.isOk()));
        }
        return currentValues;
    }
}